﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
using System.Dynamic;
using System.Reflection;
using System.Diagnostics;

namespace PerformanceProfiler
{
    public class Profiler<T> : DynamicObject where T : class
    {
        public Profiler(T subject)
        {
            LogTimes = true;
            KeepRecords = true;
            Subject = subject;
            SubjectName = null;
            profilerwatch = new StopWatch();
        }

        #region Fields

        private StopWatch profilerwatch;

        private InstanceExecutionRecord instancerecord;

        private object _lock = new object();

        #endregion

        #region Properties

        public T Subject { get; private set; }

        public string SubjectName { get; set; }

        public bool LogTimes { get; set; }

        public bool KeepRecords { get; set; }

        #endregion

        #region DynamicMembers

        public override bool TryGetMember(GetMemberBinder binder, out object result)
        {
            result = null;
            PropertyInfo pinfo = typeof(T).GetProperty(binder.Name);
            if ((pinfo != null) && (pinfo.CanRead))
            {
                var t0 = profilerwatch.MicroSeconds;
                var watch = new StopWatch();
                result = pinfo.GetValue(Subject, null);
                double d = watch.MicroSeconds;
                Log(binder.Name, t0, d);
                return true;
            }
            return false;
        }

        public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
        {
            result = null;
            MethodInfo info = typeof(T).GetMethod(binder.Name, args.Select(o => o.GetType()).ToArray());
            if ((info != null))
            {
                double t0 = profilerwatch.MicroSeconds;
                StopWatch w = new StopWatch();
                result = info.Invoke(Subject, args);
                double d = w.MicroSeconds;
                Log(binder.Name, t0, d);
                return true;
            }
            return false;
        }

        public override bool TrySetIndex(SetIndexBinder binder, object[] indexes, object value)
        {
            object attribute = typeof(T).GetCustomAttributes(
                typeof(DefaultMemberAttribute), true).FirstOrDefault();
            if (attribute != null)
            {
                DefaultMemberAttribute att = attribute as DefaultMemberAttribute;
                PropertyInfo pinfo = typeof(T).GetProperty(att.MemberName, indexes.Select(p => p.GetType()).ToArray());
                if ((pinfo != null) && (pinfo.CanWrite))
                {
                    double t0 = profilerwatch.MicroSeconds;
                    StopWatch w = new StopWatch();
                    pinfo.SetValue(Subject, value, indexes);
                    double d = w.MicroSeconds;
                    Log(String.Format("{0}.set", att.MemberName), t0, d);
                    return true;
                }
            }
            return false;
        }

        public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result)
        {
            result = null;
            object attribute = typeof(T).GetCustomAttributes(
                typeof(DefaultMemberAttribute), true).FirstOrDefault();
            if (attribute != null)
            {
                DefaultMemberAttribute att = attribute as DefaultMemberAttribute;
                PropertyInfo pinfo = typeof(T).GetProperty(att.MemberName, indexes.Select(p => p.GetType()).ToArray());
                if ((pinfo != null) && (pinfo.CanRead))
                {
                    double t0 = profilerwatch.MicroSeconds;
                    StopWatch w = new StopWatch();
                    result = pinfo.GetValue(Subject, indexes);
                    double d = w.MicroSeconds;
                    Log(String.Format("{0}.get", att.MemberName), t0, d);
                    return true;
                }
            }
            return false;
        }

        #endregion

        #region Logs

        private const char micro = (char)0x03BC;
        private void Log(string name, double start, double total)
        {
            Debug.WriteLine(String.Format("Execution time for {0}.{1} = {2} {3}s", 
                SubjectId, name, total, micro));
        }

        private string subjectid = null;
        protected string SubjectId
        {
            get 
            {
                if (subjectid == null)
                {
                    if (string.IsNullOrEmpty(SubjectName))
                        subjectid = Subject.GetType().Name;
                    else
                        subjectid = String.Format("{0}({1})", Subject.GetType().Name, SubjectName);
                }
                return subjectid;
            }
        }

        #endregion

        #region Execution Records

        protected virtual void OnMemberExecuted(string name, double start, double total)
        {
            if (LogTimes)
                Log(name, start, total);
            if (KeepRecords)
                AddExecutionRecord(name, start, total);
        }

        private void AddExecutionRecord(string name, double start, double total)
        {
            lock (_lock)
            {
                if (instancerecord == null)
                    instancerecord = ExecutionReport.GetInstanceRecord(SubjectId);
                instancerecord.AddMemberRecord(name, start, total);
            }
        }

        #endregion
    }

    public class StopWatch
    {
        public StopWatch()
        {
            QueryPerformanceFrequency(out freq);
            QueryPerformanceCounter(out t0);
        }

        #region Fields

        private long t0;
        private long freq;

        #endregion

        public void Start()
        {
            QueryPerformanceCounter(out t0);
        }

        public double MicroSeconds
        {
            get
            {
                long t1;
                QueryPerformanceCounter(out t1);
                return 1.0E6 * (t1 - t0) / freq;
            }
        }

        public double Milliseconds
        {
            get
            {
                long t1;
                QueryPerformanceCounter(out t1);
                return 1.0E3 * (t1 - t0) / freq;
            }
        }

        #region External Functions

        [DllImport("KERNEL32")]
        private static extern bool QueryPerformanceCounter(out long lpPerformanceCount);

        [DllImport("KERNEL32")]
        private static extern bool QueryPerformanceFrequency(out long lpFrequency);

        #endregion External Functions
    }

    public static class ProfilerExtensions
    {
        public static DynamicObject GetProfiler<T>(this T subject) where T : class
        {
            if (subject == null) throw new ArgumentNullException("subject");
            return new Profiler<T>(subject);
        }
    }
}
